package com.diy.sigmund.diyorder.service.impl;

import com.diy.sigmund.diycommon.entity.CustomUser;
import com.diy.sigmund.diycommon.entity.ResultMsg;
import com.diy.sigmund.diycommon.entity.base.exception.DiyRuntimeException;
import com.diy.sigmund.diycommon.entity.constant.LockConstant;
import com.diy.sigmund.diycommon.entity.enumerate.ResponseEnum;
import com.diy.sigmund.diycommon.middleware.cache.redis.RedisDistributedLock;
import com.diy.sigmund.diycommon.middleware.cache.redis.RedisService;
import com.diy.sigmund.diycommon.spring.ContextHolder;
import com.diy.sigmund.diycommon.spring.CustomApplicationContext;
import com.diy.sigmund.diycommon.util.json.JacksonUtil;
import com.diy.sigmund.diycommon.util.seq.SeqUtil;
import com.diy.sigmund.diycommon.util.time.DateUtil;
import com.diy.sigmund.diyorder.entity.bo.OdSeckillOrderBO;
import com.diy.sigmund.diyorder.entity.dto.OdSeckillOrderDTO;
import com.diy.sigmund.diyorder.entity.dto.TestBetweenAndDTO;
import com.diy.sigmund.diyorder.entity.enumerate.CommodityStatusEnum;
import com.diy.sigmund.diyorder.entity.enumerate.OrderStatusEnum;
import com.diy.sigmund.diyorder.entity.po.OdSeckillCommodity;
import com.diy.sigmund.diyorder.entity.po.OdSeckillOrder;
import com.diy.sigmund.diyorder.entity.query.CountByCommodityIdAndPhoneQuery;
import com.diy.sigmund.diyorder.feign.client.PointProcessingClient;
import com.diy.sigmund.diyorder.mapper.OdSeckillCommodityMapper;
import com.diy.sigmund.diyorder.mapper.OdSeckillOrderMapper;
import com.diy.sigmund.diyorder.service.SecKillService;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

/**
 * @author ylm-sigmund
 * @since 2021/11/3 22:08
 */
@Service
public class SecKillServiceImpl implements SecKillService {

    @Autowired
    private OdSeckillCommodityMapper odSeckillCommodityMapper;
    @Autowired
    private OdSeckillOrderMapper odSeckillOrderMapper;
    @Autowired
    private RedisService redisService;
    @Autowired
    private RedisDistributedLock redisDistributedLock;
    @Autowired
    private PointProcessingClient pointProcessingClient;
    private static final Logger log = LoggerFactory.getLogger(SecKillServiceImpl.class);

    /**
     * 本机Windows单机可以模拟高并发，是锁的使用问题，
     * 该秒杀接口逻辑分为2段：1段校验，1段数据持久化。
     * 之前校验逻辑采用了2段锁，现在将校验逻辑全部封装在一个锁里面，可满足1000QPS缓存和DB数据一致性
     */
    @Override
    public ResultMsg panicBuying(OdSeckillOrderDTO odSeckillOrderDTO) {
        CustomUser customUser = ContextHolder.getInstance().getCustomUser();
        String requestId = SeqUtil.getRequestId();
        // 锁用户，使用唯一标识符，防止用户并发，秒杀多种商品
        String lockKey = LockConstant.HANDLE_PANIC_BUYING_RECORD_KEY_PREFIX + customUser.getUserId();
        // 锁商品，防止用户并发，秒杀同一件商品
        String spanLockKey = LockConstant.SPAN_LOCK_KEY_PREFIX + odSeckillOrderDTO.getCommodityId();
        // 考虑数据持久化需要的时间，保证业务幂等。只加锁，不释放，等超时自动释放
        redisDistributedLock.tryLock(lockKey, requestId, 30);
        // 自旋锁，解决均匀分布的秒杀
        redisDistributedLock.trySpinLock(spanLockKey, requestId, 30, 1000);
        // 前置商品校验
        ResultMsg checkResult;
        try {
            checkResult = check(odSeckillOrderDTO);
        } catch (Exception e) {
            log.error("前置校验失败", e);
            redisDistributedLock.releaseLock(spanLockKey, requestId);
            redisDistributedLock.releaseLock(lockKey, requestId);
            return ResultMsg.fail(ResponseEnum.REQUEST_CONCURRENT);
        }
        if (checkResult != null) {
            redisDistributedLock.releaseLock(spanLockKey, requestId);
            redisDistributedLock.releaseLock(lockKey, requestId);
            return checkResult;
        }
        // 秒杀订单入队
        OdSeckillOrderBO odSeckillOrderBO = new OdSeckillOrderBO();
        odSeckillOrderBO.setId(SeqUtil.snowflakeId());
        odSeckillOrderBO.setCommodityId(odSeckillOrderDTO.getCommodityId());
        odSeckillOrderBO.setTransactionQuantity(odSeckillOrderDTO.getTransactionQuantity());
        odSeckillOrderBO.setHandleSuccess(Boolean.FALSE);
        odSeckillOrderBO.setLockKey1(lockKey);
        odSeckillOrderBO.setLockKey2(spanLockKey);
        odSeckillOrderBO.setLockValue(requestId);
        log.info("秒杀订单入队准备信息{}", JacksonUtil.toJson(odSeckillOrderBO));
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        CustomApplicationContext.getBean(SecKillServiceImpl.class).asyncHandle(odSeckillOrderBO, requestAttributes);
        return ResultMsg.success(odSeckillOrderBO);
    }

    @Async("customAsyncExecutor")
    public void asyncHandle(OdSeckillOrderBO odSeckillOrderBO, RequestAttributes requestAttributes) {
        // 防止上下文丢失
        RequestContextHolder.setRequestAttributes(requestAttributes);
        String commodityId = odSeckillOrderBO.getCommodityId();
        String lockKey1 = odSeckillOrderBO.getLockKey1();
        String lockKey2 = odSeckillOrderBO.getLockKey2();
        String requestId = odSeckillOrderBO.getLockValue();
        try {
            log.info("秒杀订单入队接收信息{}", JacksonUtil.toJson(odSeckillOrderBO));
            CustomUser customUser = ContextHolder.getInstance().getCustomUser();
            log.info("CustomUser：{}", customUser);
            // 消费幂等:查询id对应订单是否已存在
            String id = odSeckillOrderBO.getId();
            int countById = odSeckillOrderMapper.countById(id);
            log.info("当前id对应订单数量：{}", countById);
            if (countById > 0) {
                log.info("当前订单已入库,不需要重复操作,OdSeckillOrderBO={},customUser={}", JacksonUtil.toJson(odSeckillOrderBO),
                        JacksonUtil.toJson(customUser));
                return;
            }
            // 业务幂等:同一个商品id+同一个phone只有一个秒杀订单
            String identifier = customUser.getIdentifier();
            OdSeckillCommodity objCache = (OdSeckillCommodity) redisService.hGet(LockConstant.COMMON_KEY,
                    commodityId);
            CountByCommodityIdAndPhoneQuery countByCommodityIdAndPhoneQuery = new CountByCommodityIdAndPhoneQuery();
            countByCommodityIdAndPhoneQuery.setCommodityId(commodityId);
            countByCommodityIdAndPhoneQuery.setPhone(identifier);
            countByCommodityIdAndPhoneQuery.setAddedTime(objCache.getAddedTime());
            countByCommodityIdAndPhoneQuery.setOffShelfTime(objCache.getOffShelfTime());
            log.info("countByCommodityIdAndPhone:{}", JacksonUtil.toJson(countByCommodityIdAndPhoneQuery));
            int countByCommodityIdAndPhone = odSeckillOrderMapper.countByCommodityIdAndPhone(
                    countByCommodityIdAndPhoneQuery);
            log.info("该用户本月秒杀的订单数量：{}", countByCommodityIdAndPhone);
            if (countByCommodityIdAndPhone > 0) {
                log.info("该用户本月秒杀的订单已存在，不得重复秒杀,OdSeckillOrderBO={},customUser={}",
                        JacksonUtil.toJson(odSeckillOrderBO), JacksonUtil.toJson(customUser));
                return;
            }
            // 取库存校验
            OdSeckillCommodity odSeckillCommodity = odSeckillCommodityMapper.selectByPrimaryKey(commodityId);
            log.info("数据库中商品数据：{}", JacksonUtil.toJson(odSeckillCommodity));
            Integer inventory = odSeckillCommodity.getInventory();
            if (inventory <= 0) {
                log.info("当前商品已售罄,odSeckillCommodityDO={},customUser={}",
                        JacksonUtil.toJson(odSeckillCommodity), JacksonUtil.toJson(customUser));
                return;
            }
            // 秒杀订单入库 正式下单 带事务处理
            // 只要带事务的方法a() 可以抛出正确的异常，就会回滚事务，不论外层调用a catch时是否抛出异常
            odSeckillOrderBO = CustomApplicationContext.getBean(SecKillServiceImpl.class)
                    .handlePanicBuyingRecord(odSeckillOrderBO, odSeckillCommodity);
        } catch (Exception e) {
            // catch事务中的异常，该事务也会回滚
            log.error("asyncHandle error", e);
        } finally {
            if (!odSeckillOrderBO.isHandleResult()) {
                // 回滚库存
                Long incrInventory = redisService.incr(LockConstant.INVENTORY_KEY_PREFIX + commodityId,
                        odSeckillOrderBO.getTransactionQuantity());
                log.info("处理失败，库存回滚后：{}", incrInventory);
            }
            redisDistributedLock.releaseLock(lockKey2, requestId);
            redisDistributedLock.releaseLock(lockKey1, requestId);
        }
    }

    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, timeout = 30, readOnly = false, rollbackFor = {
            Exception.class, RuntimeException.class})
    public OdSeckillOrderBO handlePanicBuyingRecord(OdSeckillOrderBO odSeckillOrderBO,
            OdSeckillCommodity odSeckillCommodity) {
        CustomUser customUser = ContextHolder.getInstance().getCustomUser();

        OdSeckillOrder odSeckillOrder = new OdSeckillOrder();
        BeanUtils.copyProperties(odSeckillOrderBO, odSeckillOrder);
        odSeckillOrder.setTransactionAmount(odSeckillCommodity.getPrice());
        odSeckillOrder.setPhone(customUser.getIdentifier());

        String commodityId = odSeckillCommodity.getId();
        Integer transactionQuantity = odSeckillOrder.getTransactionQuantity();
        int decrOneInventory = 0;
        try {
            decrOneInventory = odSeckillCommodityMapper.decrOneInventory(commodityId, transactionQuantity);
        } catch (Exception e) {
            log.error("商品减库存[异常],事务回滚,odSeckillCommodityDO={},customUser={}",
                    JacksonUtil.toJson(odSeckillCommodity), JacksonUtil.toJson(customUser));
            throw new DiyRuntimeException(ResponseEnum.DB_INVENTORY_UPDATE_FAIL);
        }
        if (decrOneInventory != 1) {
            log.warn("商品减库存[失败],事务回滚,odSeckillCommodityDO={},customUser={}",
                    JacksonUtil.toJson(odSeckillCommodity), JacksonUtil.toJson(customUser));
            throw new DiyRuntimeException(ResponseEnum.DB_INVENTORY_UPDATE_FAIL);
        }
        // 入库
        odSeckillOrder.setOrderStatus(OrderStatusEnum.SUCCESS.getCode());
        int insertOrderResult = 0;
        try {
            insertOrderResult = odSeckillOrderMapper.insertSelective(odSeckillOrder);
        } catch (Exception e) {
            log.error("秒杀订单入库[异常],事务回滚,odSeckillCommodityDO={},customUser={}",
                    JacksonUtil.toJson(odSeckillCommodity), JacksonUtil.toJson(customUser));
            throw new DiyRuntimeException(ResponseEnum.ORDER_ENDURANCE_FAIL);
        }
        if (insertOrderResult != 1) {
            log.warn("秒杀订单入库[失败],事务回滚,odSeckillCommodityDO={},customUser={}",
                    JacksonUtil.toJson(odSeckillCommodity), JacksonUtil.toJson(customUser));
            throw new DiyRuntimeException(ResponseEnum.ORDER_ENDURANCE_FAIL);
        }
        // 扣积分，后置，减少该业务的回滚代码
        ResultMsg decrease = null;
        try {
            decrease = pointProcessingClient.decrease();
        } catch (Exception e) {
            log.error("积分扣减失败", e);
        }
        log.info("pointProcessingClient.decrease() {}", JacksonUtil.toJson(decrease));
        if (!Objects.equals(ResponseEnum.SUCCESS.getCode(),
                Optional.ofNullable(decrease).map(ResultMsg::getCode).orElse("500"))) {
            log.warn("扣减积分失败,事务回滚,odSeckillCommodityDO={},customUser={}",
                    JacksonUtil.toJson(odSeckillCommodity), JacksonUtil.toJson(customUser));
            // throw new DiyRuntimeException(ResponseEnum.DEDUCTING_THE_INTEGRAL_FAIL);
        }
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            log.error("模拟业务执行用时", e);
        }
        odSeckillOrderBO.setHandleSuccess(Boolean.TRUE);
        return odSeckillOrderBO;
    }

    private ResultMsg check(OdSeckillOrderDTO odSeckillOrderDTO) {
        String commodityId = odSeckillOrderDTO.getCommodityId();
        OdSeckillCommodity objCache = (OdSeckillCommodity) redisService.hGet(LockConstant.COMMON_KEY, commodityId);
        log.info("缓存中的OdSeckillCommodityDO {}", JacksonUtil.toJson(objCache));
        if (Objects.isNull(objCache) || !redisService.exists(LockConstant.INVENTORY_KEY_PREFIX + commodityId)) {
            int timeout = 300;
            OdSeckillCommodity odSeckillCommodity = odSeckillCommodityMapper.selectByPrimaryKey(commodityId);
            log.info("数据库中OdSeckillCommodityDO的数据：{}", JacksonUtil.toJson(odSeckillCommodity));
            if (Objects.isNull(odSeckillCommodity)) {
                int timeoutForProtect = 10;
                // 解决缓存穿透，把不存在的商品id也缓存起来
                redisService.hSet(LockConstant.COMMON_KEY, commodityId, odSeckillCommodity, timeoutForProtect,
                        TimeUnit.SECONDS);
                return ResultMsg.fail(ResponseEnum.ILLEGAL_PRODUCT_ID);
            }
            redisService.hSet(LockConstant.COMMON_KEY, commodityId, odSeckillCommodity, timeout,
                    TimeUnit.SECONDS);
            redisService.set(LockConstant.INVENTORY_KEY_PREFIX + commodityId, odSeckillCommodity.getInventory(),
                    timeout, TimeUnit.SECONDS);
            objCache = odSeckillCommodity;
        }
        // 上下架状态校验
        if (objCache.getStatus().equals(CommodityStatusEnum.OFF_THE_SHELF.getCode())) {
            return ResultMsg.fail(ResponseEnum.OFF_THE_SHELF);
        }
        // 上下架时间校验
        Date now = new Date();
        if (objCache.getAddedTime().after(now) || objCache.getOffShelfTime().before(now)) {
            return ResultMsg.fail(ResponseEnum.OFF_THE_SHELF);
        }
        // 库存校验
        Integer inventory = (Integer) redisService.get(LockConstant.INVENTORY_KEY_PREFIX + commodityId);
        log.info("缓存中的库存：{}", inventory);
        if (Optional.ofNullable(inventory).orElse(0) <= 0) {
            return ResultMsg.fail(ResponseEnum.SOLD_OUT);
        }
        // 前置预减库存
        int currentInventory = redisService.decrInventory(
                LockConstant.INVENTORY_KEY_PREFIX + commodityId, odSeckillOrderDTO.getTransactionQuantity());
        log.info("缓存中该商品预减库存后的库存：{}", currentInventory);
        if (currentInventory < 0) {
            return ResultMsg.fail(ResponseEnum.SOLD_OUT);
        }
        return null;
    }

    @Override
    public ResultMsg getOrder(OdSeckillOrderDTO odSeckillOrderDTO) {
        OdSeckillOrder odSeckillOrder = odSeckillOrderMapper.selectByPrimaryKey(odSeckillOrderDTO.getId());
        if (Objects.isNull(odSeckillOrder)) {
            return ResultMsg.fail(ResponseEnum.ORDER_QUERY_DO_NOT_EXIST);
        }
        return ResultMsg.success(odSeckillOrder);
    }

    @Override
    public ResultMsg decrOneInventory(String commodityId) {
        commodityId = "1001";
        // 纯粹更新数据库，不用加锁，因为数据库行锁的存在
        // 但是如果先查询再更新，为了保证数据的一致性，还是要加锁的
        int i = odSeckillCommodityMapper.decrOneInventory(commodityId, 0);
        return ResultMsg.success(i);
    }

    @Override
    public ResultMsg testBetweenAnd(TestBetweenAndDTO testBetweenAndDTO) {
        testBetweenAndDTO.setId("1001");
        List<Date> list = new ArrayList<>();
        Date date1 = DateUtil.parseStrToDate("20211126125958999", DateUtil.DATE_TIME_FORMAT_YYYYMMDDHHMISSSSS);
        Date date2 = DateUtil.parseStrToDate("20211126125959000", DateUtil.DATE_TIME_FORMAT_YYYYMMDDHHMISSSSS);
        Date date3 = DateUtil.parseStrToDate("20211126125959001", DateUtil.DATE_TIME_FORMAT_YYYYMMDDHHMISSSSS);
        Date date4 = DateUtil.parseStrToDate("20211126095959999", DateUtil.DATE_TIME_FORMAT_YYYYMMDDHHMISSSSS);
        Date date5 = DateUtil.parseStrToDate("20211126100000000", DateUtil.DATE_TIME_FORMAT_YYYYMMDDHHMISSSSS);
        Date date6 = DateUtil.parseStrToDate("20211126100000001", DateUtil.DATE_TIME_FORMAT_YYYYMMDDHHMISSSSS);
        list.add(date1);
        list.add(date2);
        list.add(date3);
        list.add(date4);
        list.add(date5);
        list.add(date6);
        for (Date date : list) {
            log.info("CurrentDateTime::{}", DateUtil.parseDateToStr(date, DateUtil.DATE_TIME_FORMAT_YYYYMMDDHHMISSSSS));
            testBetweenAndDTO.setCurrentDateTime(date);
            log.info("testBetweenAndDTO:{}", JacksonUtil.toJson(testBetweenAndDTO));
            OdSeckillCommodity odSeckillCommodity = odSeckillCommodityMapper.testBetweenAnd(testBetweenAndDTO);
            log.info("odSeckillCommodityDO:{}", JacksonUtil.toJson(odSeckillCommodity));
        }

        return ResultMsg.success();
    }
}
